package org.dding.core.instrument;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.dding.core.log.ILogger;
import org.dding.core.log.LoggerFactory;
import org.dding.core.util.FileExtUtils;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

public class MscLoader {

	private static MscLoader mscLoader = new MscLoader();

	public static MscLoader getInstence() {
		return mscLoader;
	}

	private ILogger logger = LoggerFactory.getLogger(getClass());
	private final ConcurrentHashMap<String, List<ClassInfo>> annotated = new ConcurrentHashMap<String, List<ClassInfo>>();
	private ClassLoader classLoader;

	public synchronized void readWithASM(File[] files, ClassLoader classLoader) {
		this.classLoader = classLoader;
		Map<String, byte[]> classBytesArray = new HashMap<String, byte[]>();
		for (File file : files) {
			try {
				classBytesArray.putAll(FileExtUtils.getClassBytesMap(file));
			} catch (IOException e) {
				logger.warn("Failed to get Class names from file: " + file);
			}
		}
		for (Map.Entry<String, byte[]> entry : classBytesArray.entrySet()) {
			readClass(entry.getKey(), entry.getValue());
		}
	}

	public synchronized void clear() {
		annotated.clear();
		classLoader = null;
	}

	/**
	 * 查找被 Annotation 标注的Class列表
	 * 
	 * @param annotation
	 *            annotation
	 */
	public Class<?>[] findClassAnnotatedWith(
			Class<? extends Annotation> annotation) {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		List<ClassInfo> infos = getAnnotationInfos(annotation.getName());
		for (ClassInfo classInfo : infos) {
			try {
				ClassLoader cl = classInfo.getClassLoader();
				Class<?> clazz = cl.loadClass(classInfo.getName());
				if (clazz.isAnnotationPresent(annotation)) {
					classes.add(clazz);
				}
			} catch (ClassNotFoundException e) {
				logger.warn(e,"Exception occupied while findClassAnnotatedWith "
						+ annotation);
			}
		}
		return classes.toArray(new Class[classes.size()]);
	}

	private void readClass(String className, byte[] classBytes) {
		try {
			ClassReader classReader = new ClassReader(classBytes);
			classReader.accept(new ClassInfoBuildingVisitor(),
					ClassReader.SKIP_DEBUG);
		} catch (RuntimeException e) {
			logger.error("Error to read class: " + className, e);
			throw e;
		}
	}

	private List<ClassInfo> getAnnotationInfos(String name) {
		List<ClassInfo> infos = annotated.get(name);
		if (infos == null) {
			infos = new ArrayList<ClassInfo>();
			annotated.put(name, infos);
		}
		return infos;
	}

	class ClassInfoBuildingVisitor implements ClassVisitor, AnnotationVisitor {
		private AnnotatableInfo info;

		public ClassInfoBuildingVisitor() {
		}

		public ClassInfoBuildingVisitor(AnnotatableInfo info) {
			this.info = info;
		}

		public void visit(int version, int access, String name,
				String signature, String superName, String[] interfaces) {
			if (!name.endsWith("package-info")) {
				ClassInfo classInfo = new ClassInfo(javaName(name), classLoader);
				info = classInfo;
			}
		}

		public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
			AnnotationInfo annotationInfo = new AnnotationInfo(desc);
			info.getAnnotations().add(annotationInfo);
			if (info instanceof ClassInfo) {
				getAnnotationInfos(annotationInfo.getName()).add(
						(ClassInfo) info);
			}
			return new ClassInfoBuildingVisitor(annotationInfo);
		}

		private String javaName(String name) {
			return (name == null) ? null : name.replace('/', '.');
		}

		// --------- not need to implement
		public void visitAttribute(Attribute attribute) {
		}

		public void visitEnd() {
		}

		public FieldVisitor visitField(int i, String string, String string1,
				String string2, Object object) {
			return null;
		}

		public void visitInnerClass(String string, String string1,
				String string2, int i) {
		}

		public MethodVisitor visitMethod(int i, String string, String string1,
				String string2, String[] strings) {
			return null;
		}

		public void visitOuterClass(String string, String string1,
				String string2) {
		}

		public void visitSource(String string, String string1) {
		}

		public void visit(String string, Object object) {
		}

		public AnnotationVisitor visitAnnotation(String string, String string1) {
			return null;
		}

		public AnnotationVisitor visitArray(String string) {
			return null;
		}

		public void visitEnum(String string, String string1, String string2) {
		}
	}

	abstract class AnnotatableInfo {
		private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();

		public AnnotatableInfo() {
		}

		public AnnotatableInfo(AnnotatedElement element) {
			for (Annotation annotation : element.getAnnotations()) {
				annotations.add(new AnnotationInfo(annotation.annotationType()
						.getName()));
			}
		}

		public List<AnnotationInfo> getAnnotations() {
			return annotations;
		}

		public abstract String getName();
	}

	class ClassInfo extends AnnotatableInfo {
		private final String className;
		private final ClassLoader classLoader;

		public ClassInfo(String className, ClassLoader classLoader) {
			this.className = className;
			this.classLoader = classLoader;
		}

		@Override
		public String getName() {
			return className;
		}

		public ClassLoader getClassLoader() {
			return classLoader;
		}

	}

	class AnnotationInfo extends AnnotatableInfo {
		private final String name;

		public AnnotationInfo(Annotation annotation) {
			this(annotation.getClass().getName());
		}

		public AnnotationInfo(Class<? extends Annotation> annotation) {
			this.name = annotation.getName().intern();
		}

		public AnnotationInfo(String name) {
			name = name.replaceAll("^L|;$", "");
			name = name.replace('/', '.');
			this.name = name.intern();
		}

		public String getName() {
			return name;
		}

		public String toString() {
			return getName();
		}
	}

}
